package jdec.jdt.ls.decompiler.procyon;

import static org.eclipse.jdt.ls.core.internal.handlers.MapFlattener.getValue;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URI;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.time.StopWatch;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.core.IClassFile;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.ls.core.internal.JDTUtils;
import org.eclipse.jdt.ls.core.internal.preferences.Preferences;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.strobel.assembler.InputTypeLoader;
import com.strobel.assembler.metadata.ClasspathTypeLoader;
import com.strobel.assembler.metadata.DeobfuscationUtilities;
import com.strobel.assembler.metadata.ITypeLoader;
import com.strobel.assembler.metadata.MetadataSystem;
import com.strobel.assembler.metadata.TypeDefinition;
import com.strobel.assembler.metadata.TypeReference;
import com.strobel.decompiler.DecompilationOptions;
import com.strobel.decompiler.DecompilerSettings;
import com.strobel.decompiler.LineNumberFormatter;
import com.strobel.decompiler.PlainTextOutput;
import com.strobel.decompiler.languages.LineNumberPosition;
import com.strobel.decompiler.languages.TypeDecompilationResults;
import com.strobel.decompiler.languages.java.JavaFormattingOptions;
import jdec.jdt.ls.decompiler.common.CachingDecompiler;
import jdec.jdt.ls.decompiler.common.JavaDecompilerPlugin;
import jdec.jdt.ls.decompiler.common.SupportDecompilerType;
import jdec.jdt.ls.decompiler.common.util.FileUtil;
import jdec.jdt.ls.decompiler.common.util.JarClassExtractor;
import jdec.jdt.ls.decompiler.common.util.Logger;

public class ProcyonDecompiler extends CachingDecompiler {
    DecompilerSettings settings = DecompilerSettings.javaDefaults();

    public static final String OPTIONS_KEY = "java.decompiler.procyon";
    public static final String DECOMPILED_HEADER = " // Source code is unavailable, and was generated by the Procyon decompiler.\n";

    @SuppressWarnings("unchecked")
    @Override
    public void setDecompilePreferences(Preferences preferences) {
        Object options = getValue(preferences.asMap(), ProcyonDecompiler.OPTIONS_KEY);
        if (options instanceof Map) {
            Map<String, Object> optionsMap = (Map<String, Object>) options;

            // prefix keys with '_'
            for (String key : optionsMap.keySet()) {
                if (!key.startsWith("_")) {
                    optionsMap.put("_" + key, optionsMap.get(key));
                    optionsMap.remove(key);
                }
            }

            Gson gson = new Gson();
            JsonElement jsonElement = gson.toJsonTree(options);
            settings = gson.fromJson(jsonElement, DecompilerSettings.class);
            settings.setJavaFormattingOptions(JavaFormattingOptions.createDefault());
            if (!((Map<?, ?>) options).containsKey("forceExplicitImports")) {
                settings.setForceExplicitImports(true);
            }
        }

        // debugger
        settings.setShowDebugLineNumbers(true);
        settings.setIncludeLineNumbersInBytecode(true);
    }

    @Override
    protected String decompileContent(URI uri, IProgressMonitor monitor) throws CoreException {
        IClassFile classFile = JDTUtils.resolveClassFile(uri);
        if (classFile != null) {
            return decompileContent(classFile.getType(), monitor);
        }

        String path = uri.getPath();
        return getContent(new InputTypeLoader(new ClasspathTypeLoader()), path, monitor);
    }

    @Override
    protected String decompileContent(IType type, IProgressMonitor monitor) throws CoreException {
        IClassFile classFile = type.getClassFile();
        return getContent(new JDTTypeLoader(classFile), JDTTypeLoader.FAKE_CLASS_NAME, monitor);
    }

    private String getContent(ITypeLoader typeLoader, String path, IProgressMonitor monitor) throws CoreException {
        settings.setTypeLoader(typeLoader);
        DecompilationOptions decompilationOptions = new DecompilationOptions();
        decompilationOptions.setSettings(settings);
        decompilationOptions.setFullDecompilation(true);

        MetadataSystem metadataSystem = new NoRetryMetadataSystem(typeLoader);
        metadataSystem.setEagerMethodLoadingEnabled(false);

        if (typeLoader instanceof JDTTypeLoader) {
            ((JDTTypeLoader) typeLoader).setMetadataResolver(metadataSystem);
        }

        TypeReference type = metadataSystem.lookupType(path);
        if (type == null) {
            return null;
        }
        TypeDefinition resolvedType = type.resolve();
        if (resolvedType == null) {
            return null;
        }

        DeobfuscationUtilities.processType(resolvedType);

        try (StringWriter stringwriter = new StringWriter();) {

            PlainTextOutput output = new PlainTextOutput(stringwriter);
            output.setUnicodeOutputEnabled(decompilationOptions.getSettings().isUnicodeOutputEnabled());
            settings.getLanguage().decompileType(resolvedType, output, decompilationOptions);

            /* SL:478 */
            String code = DECOMPILED_HEADER + stringwriter.toString();
            // return new EclipseFormater().formatSource(code);
            return code;
        } catch (Throwable t) {
            throw new CoreException(new Status(IStatus.ERROR, "jdec.jdt.ls.decompiler.cfr", "Error decompiling", t));
        }
    }

    public String getContent(String root, String packege, String className) throws CoreException {
        File workingDir = new File(root + "/" + packege); //$NON-NLS-1$
        final String classPathStr = new File(workingDir, className).getAbsolutePath();
        DecompilationOptions decompilationOptions = new DecompilationOptions();

        DecompilerSettings settings = DecompilerSettings.javaDefaults();
        // debugger
        settings.setTypeLoader(new com.strobel.assembler.InputTypeLoader());
        settings.setForceExplicitImports(true);

        decompilationOptions.setSettings(settings);
        decompilationOptions.setFullDecompilation(true);

        MetadataSystem metadataSystem = new NoRetryMetadataSystem(decompilationOptions.getSettings().getTypeLoader());
        metadataSystem.setEagerMethodLoadingEnabled(false);

        TypeReference type = metadataSystem.lookupType(classPathStr);
        TypeDefinition resolvedType;
        if (type == null || (resolvedType = type.resolve()) == null) {
            System.err.printf("!!! ERROR: Failed to load class %s.\n", //$NON-NLS-1$
                    classPathStr);
            throw new CoreException(
                    new Status(IStatus.ERROR, "java.decompiler.procyon", "Error decompiling !!! ERROR: Failed to load class" + classPathStr));
        }

        DeobfuscationUtilities.processType(resolvedType);
        File classFile = new File(JavaDecompilerPlugin.tempDir(), System.currentTimeMillis() + className);
        try {
            TypeDecompilationResults results;
            try (Writer writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(classFile)))) {
                PlainTextOutput output = new PlainTextOutput(writer);
                output.setUnicodeOutputEnabled(decompilationOptions.getSettings().isUnicodeOutputEnabled());
                results = decompilationOptions.getSettings().getLanguage().decompileType(resolvedType, output, decompilationOptions);
            }

            List<LineNumberPosition> lineNumberPositions = results.getLineNumberPositions();
            EnumSet<LineNumberFormatter.LineNumberOption> lineNumberOptions = EnumSet.noneOf(LineNumberFormatter.LineNumberOption.class);
            lineNumberOptions.add(LineNumberFormatter.LineNumberOption.LEADING_COMMENTS);
            lineNumberOptions.add(LineNumberFormatter.LineNumberOption.STRETCHED);
            LineNumberFormatter lineFormatter = new LineNumberFormatter(classFile, lineNumberPositions, lineNumberOptions);

            lineFormatter.reformatFile();
        } catch (IOException e) {
            Logger.error(e);
        }

        String source = FileUtil.getContent(classFile);

        classFile.delete();

        Pattern wp = Pattern.compile("/\\*.+?\\*/", Pattern.DOTALL); //$NON-NLS-1$
        Matcher m = wp.matcher(source);
        while (m.find()) {
            if (m.group().matches("/\\*\\s*\\d*\\s*\\*/")) {
                continue;
            }
            String group = m.group();
            group = group.replace("/*", ""); //$NON-NLS-1$ //$NON-NLS-2$
            group = group.replace("*/", ""); //$NON-NLS-1$ //$NON-NLS-2$
            group = group.replace("*", ""); //$NON-NLS-1$ //$NON-NLS-2$

            source = source.replace(m.group(), ""); //$NON-NLS-1$
        }

        return source;
    }

    @Override
    protected String decompileFromArchive(String archivePath, String packege, String className, IProgressMonitor monitor) throws CoreException {
        File workingDir = new File(JavaDecompilerPlugin.tempDir());

        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        try {
            workingDir.mkdirs();
            JarClassExtractor.extract(archivePath, packege, className, true, workingDir.getAbsolutePath());
            return getContent(workingDir.getAbsolutePath(), "", className); //$NON-NLS-1$
        } catch (Exception e) {
            // JavaDecompilerPlugin.logError(e, e.getMessage());
            throw new CoreException(new Status(IStatus.ERROR, "java.decompiler.procyon", "Error decompiling", e));
        } finally {
            FileUtil.deltree(workingDir);
        }
    }

    @Override
    protected String getDecompilerType() {
        return SupportDecompilerType.PROCYON;
    }

}
